﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;

namespace Simhopp.Common
{
    /// <summary>
    /// The Dive class provides a description of a dive.
    /// </summary>
    [Serializable]
    public class Dive : IComparable, IDatabaseObject, ISerializable, INotifyPropertyChanged
    {
        #region FIELD REGION
        private int id;
        private int diverId;
        private DivingType diveType;
        private ObservableCollection<Score> scores;
        private double totalScore;

        public event PropertyChangedEventHandler PropertyChanged;

        #endregion
        #region CONSTRUCTOR REGION
        /// <summary>
        /// Default constructor
        /// </summary>
        public Dive()
        {
            id = -1;
            diveType = new DivingType
            {
                ID = "",
                TypeName = "",
                Height = Heights.OneMeter,
                DifficultyFactor = 0.0
            };
            scores = new ObservableCollection<Score>();
        }
        /// <summary>
        /// Constructor taking arguments.
        /// </summary>
        /// <param name="type">The type of the dive.</param>
        public Dive(DivingType type)
        {
            diveType = type;
            scores = new ObservableCollection<Score>();
        }
        #endregion
        #region PROPERTY REGION
        /// <summary>
        /// Property ID gets or sets the type ID string.
        /// </summary>
        public String DiveTypeId
        {
            get { return diveType.ID; }
            set { diveType.ID = value; }
        }
        /// <summary>
        /// Property KeyID gets or sets the KeyID of the diveType.
        /// </summary>
        public int KeyId
        {
            get { return diveType.KeyId; }
            set { diveType.KeyId = value; }
        }
        /// <summary>
        /// Property Height gets or sets the height of the dive.
        /// </summary>
        public Heights Height
        {
            get { return diveType.Height; }
            set { diveType.Height = value; }
        }
        /// <summary>
        /// Property TypeName gets or sets the type name.
        /// </summary>
        public String TypeName
        {
            get { return diveType.TypeName; }
            set { diveType.TypeName = value; }
        }
        /// <summary>
        /// Property DiveType gets or sets the dive type.
        /// </summary>
        public DivingType DiveType
        {
            get { return diveType; }
            set { diveType = value; }
        }
        /// <summary>
        /// Property DifficultyFactor gets or sets the difficulty factor.
        /// </summary>
        public double DifficultyFactor
        {
            get { return diveType.DifficultyFactor; }
            set { diveType.DifficultyFactor = value; }
        }
        /// <summary>
        /// Property ScoreCount gets the number of scores currently associated with the dive.
        /// </summary>
        public int ScoreCount
        {
            get { return scores.Count; }
        }
        /// <summary>
        /// Property Scores gets or sets the field scores
        /// </summary>
        public ObservableCollection<Score> Scores
        {
            get { return scores; }
            set { scores = value; }
        }
        /// <summary>
        /// Property TotalScore gets the total score of the dive.
        /// </summary>
        /// 
        public double TotalScore
        {
            get
            {
                totalScore = Math.Round(totalScore, 4);
                return totalScore;
            }
            set
            {
                totalScore = value;
                NotifyPropertyChanged();
            }
        }
        /// <summary>
        /// Property Id gets or sets the id.
        /// </summary>
        public int Id
        {
            get { return id; }
            set 
            { 
                id = value;
                foreach (var score in scores)
                    score.DiveId = id;                    
            }
        }
        /// <summary>
        /// Property DiverId gets or sets the diverId.
        /// </summary>
        public int DiverId
        {
            get { return diverId; }
            set { diverId = value; }
        }
        #endregion
        #region HELPER METHOD REGION
        /// <summary>
        /// Gets the total score of the dive
        /// </summary>
        /// <returns>the total score</returns>
        private double GetTotalScore()
        {
            var sum = 0.0;
            var max = scores[0].Value;
            var min = scores[0].Value;

            foreach (var score in scores)
            {
                if (score.Value > max)
                    max = score.Value;
                else if (score.Value < min)
                    min = score.Value;
                sum += score.Value;
            }
            sum = ((sum - max - min) * DifficultyFactor)/(Scores.Count-2);
            return sum;
        }
        /// <summary>
        /// Notifies to the subscribers that a property has changed.
        /// </summary>
        /// <param name="propertyName"></param>
        private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
        #endregion
        #region METHOD REGION
        /// <summary>
        /// Checks if two instances of the object are identical
        /// </summary>
        /// <param name="obj">The object to compare with</param>
        /// <returns>True/False</returns>
        public override bool Equals(object obj)
        {
            return ((obj as Dive) != null) && (obj as Dive).Id == Id;
        }

        /// <summary>
        /// Used in addition to Equals method
        /// </summary>
        /// <returns>A unique id</returns>
        public override int GetHashCode()
        {
            return id;
        }

        /// <summary>
        /// Adds a score to the score list.
        /// </summary>
        /// <param name="score">This is the Score to add.</param>
        public void AddScore(Score score)
        {
            if (score.Judge.HasSubmittedScore)
            {
                throw new ArgumentException("That judge has already submitted a score.");
            }
            scores.Add(score);
        }

        public void UpdateTotalScore()
        {
            this.TotalScore = GetTotalScore();
        }
        /// <summary>
        /// Removes the specified score from the score list.
        /// </summary>
        /// <param name="score">The Score object to remove.</param>
        public void RemoveScore(Score score)
        {
            if (scores.Count <= 0)
            {
                throw new ScoreListEmptyException("The score list is empty.");
            }
            if (!scores.Remove(score))
            {
                throw new ArgumentException("The score is not in the list.");
            }
        }

        /// <summary>
        /// Removes all the scores from the list.
        /// </summary>
        public void RemoveAllScores()
        {
            scores.Clear();
        }

        /// <summary>
        /// Compares the divers score in comparison to anothter divers score
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public int CompareTo(Object obj)
        {
            if (obj == null)
                return 1;

            var otherDive = obj as Dive;
            if (otherDive != null)
            {
                return TotalScore.CompareTo(otherDive.TotalScore);
            }
            throw new ArgumentException("Object is not a Diver.");
        }
        #endregion
        #region DATABASE REGION

        /// <summary>
        /// Creates a dictionary from the object where member field names are mapped to their corresponding values
        /// </summary>
        /// <returns>A dictionary</returns>
        public Dictionary<String, Object> GetDictionary()
        {
            try
            {
                var dictionary = new Dictionary<String, Object>
                {
                    {"Id", Id.ToString(CultureInfo.InvariantCulture)},
                    {"Type", KeyId.ToString(CultureInfo.InvariantCulture)},
                    {"DiverId", DiverId},
                    {"TotalScore", TotalScore.ToString(CultureInfo.InvariantCulture).Replace(",", ".")}
                };
                return dictionary;
            }
            catch (Exception e)
            {
                MyLogger.Error("Failed to create dictionary from Dive object - " + e.Message);
                return null;
            }
        }

        /// <summary>
        /// Creates an Dive object from a passed dictionary
        /// </summary>
        /// <param name="data">A dictionary where database field names are mapped to their corresponding values</param>
        /// <returns>A Dive object</returns>
        public static Dive GetFromDictionary(Dictionary<String, Object> data)
        {
            try
            {
                var dive = new Dive
                {
                    Id = (int)data["Id"],
                    KeyId = (int)data["Type"],
                    DiverId = (int)data["DiverId"],
                    Scores = new ObservableCollection<Score>() // ScoreMap.GetAllScoresForOneDive((int)data["Id"])
                };
                if (!Convert.ToString(data["TotalScore"]).Equals(""))
                    dive.TotalScore = (double) data["TotalScore"];
                return dive;
            }
            catch (Exception e)
            {
                MyLogger.Error("Failed to create Diver object from dictionary - " + e.Message);
                return null;
            }
        }

        /// <summary>
        /// Returns the name of the Id field used in the corresponding database-table
        /// </summary>
        /// <returns></returns>
        public String GetIdFieldName()
        {
            return "Id";
        }

        /// <summary>
        /// Returns the name of the database-table containing this type
        /// </summary>
        /// <returns></returns>
        public String GetTableName()
        {
            return "Dives";
        }

        #endregion
        #region SERIALIZE/DESERIALIZE
        /// <summary>
        /// Adds the class specific information used in the serialization of an object.
        /// </summary>
        /// <param name="info">The class specific information.</param>
        /// <param name="context">Describes the source and destination of a given serialized stream.</param>
        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.AddValue("id", id, typeof(int));
            info.AddValue("diverId", diverId, typeof(int));
            info.AddValue("diveType", diveType, typeof(DivingType));
            info.AddValue("scores", scores, typeof(ObservableCollection<Score>));
            info.AddValue("totalScore", totalScore, typeof(double));
        }
        /// <summary>
        /// Constructor used in the deserialization of an object.
        /// </summary>
        /// <param name="info">The class specific information.</param>
        /// <param name="context">Describes the source and destination of a given serialized stream.</param>
        protected Dive(SerializationInfo info, StreamingContext context)
        {
            id = (int)info.GetValue("id", typeof(int));
            diverId = (int)info.GetValue("diverId", typeof(int));
            diveType = (DivingType)info.GetValue("diveType", typeof(DivingType));
            scores = (ObservableCollection<Score>)info.GetValue("scores", typeof(ObservableCollection<Score>));
            totalScore = (double)info.GetValue("totalScore", typeof(double));
        }
        #endregion
    }
}
